Programmeerimise aine lisamaterjalid
Õpiku esimeses peatükis sai uuritud math moodulit ning selle funktsioone ja konstante. Pythonis on palju rohkem selliseid mooduleid erinevate probleemide lahendamiseks.
Näiteks:
Pythonisse sisse ehitatud moodulid teevad lahendused sellistele küsimustele lihtsaks. Selles peatükis vaatame mõne huvitavama mooduli kasutamise näited. Uurime, kuidas moodulid üldse töötavad ja kuidas teha enda lihtne moodul.
Selle peatüki läbimiseks piisab Pythoni installatsioonist ning õpiku esimese peatüki lugemisest. Väikesel määral käsitletakse andmestruktuure, mida pole veel tutvustatud.
Et kõigest aru saada, on soovitatav tutvuda funktsioonidega (peatükk 4) ja järjenditega (peatükk 7).
Tuletame kõigepealt meelde, kuidas mooduleid importida. Seda saab teha import lause abil:
>>> import math |
Moodulitele saab anda ka nimesid as lausega. Seda tehakse pigem harva ja pikemate või väga sagedasti kasutatavate nimedega. Tavaks on lühendada pandas moodul pd-ks ja numpy moodul np-ks.
>>> import math as m |
Moodulitest saab importida ainult vajalikud muutujad kasutades from lauset. Tervet moodulit sellega ei impordita. Mitu muutujat saab eraldada komaga.
>>> from math import pi, cos |
Tärni abil saab importida kõik mooduli muutujad.
>>> from math import * |
Pythoniga tuleb kaasa palju mooduleid, mida nimetatakse kollektiivselt standardteegiks. Nende täisnimekiri on saadaval Pythoni dokumentatsioonis, aga vaatame lähemalt mõnda huvitavamat.
Selle mooduli abil saab ligi matemaatilistele funktsioonidele, mida Pythonis vaikimisi ei ole. Täisdokumentatsioon: https://docs.python.org/3/library/math.html
Importimine:
>>> import math |
Kasulikud konstandid ja funktsioonid:
>>> math.pi # pii |
>>> math.sqrt(2) # ruutjuur |
>>> math.isclose(1.45, 1.46, rel_tol=0.01) # tõeväärtus, kas kaks arvu on lähestikku rel_tol piires |
Mõned kasulikud matemaatilised funktsioonid on Pythonisse juba sisse ehitatud:
>>> round(1.7) # ümardamine |
Selle mooduliga saab genereerida pseudojuhuslikke arve, et tuua programmidesse juhuslikkust. Täisdokumentatsioon: https://docs.python.org/3/library/random.html
Importimine:
>>> import random |
Kasulikud funktsioonid:
>>> random.randint(1, 10) # juhuslik täisarv kahe arvu vahel |
>>> random.random() # juhuslik ujukomaarv 0 ja 1 vahel |
>>> pakk = ["ärtu", "ruutu", "poti", "risti"] |
Selle mooduli abil saab käsitleda kuupäevi ja aegu. Siin näites luuakse objekte, millest räägitakse täpsemalt peatükis "Objektorienteeritud programmeerimine". Praeguseks olgu need lihtsalt keerulised muutujad, millega saab erinevaid asju teha. Täisdokumentatsioon: https://docs.python.org/3/library/datetime.html
Importimine:
>>> import datetime |
Datetime objekti loomine (aasta, kuu, päev, tund, minut, sekund):
>>> kuupäev = datetime.datetime(2020, 2, 29, 12, 34, 56) |
Praeguse ajahetke saamine:
>>> praegu = datetime.datetime.now() |
Datetime objekti vormindamine sõneks:
>>> praegu.strftime("%Y-%m-%d %H:%M:%S") |
Vormindamise märgendite nimekiri: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
Datetime objekti parsimine sõnest.
>>> datetime.datetime.strptime("2020-02-29 18:20:33", "%Y-%m-%d %H:%M:%S") |
Aegadele saab kestusi juurde liita ja maha lahutada, mille kaudu tekib uus datetime objekt. Liidame enne loodud kuupäevale juurde 13 tundi ja 37 minutit:
>>> kestus = datetime.timedelta(hours=13, minutes=37) |
Seega homse kuupäeva saab kätte nii:
>>> homme = praegu + datetime.timedelta(days=1) |
Selle mooduliga saab ligi operatsioonisüsteemi funktsionaalsusele, näiteks failide asukohtadele.
Täisdokumentatsioon: https://docs.python.org/3/library/os.html
Importimine:
>>> import os |
Praeguse tee (current working directory) saamine:
>>> os.getcwd() |
Failinimede järjendi saamine praeguses kaustas ja määratud kaustas:
>>> os.listdir() |
Failinime muutmine:
>>> os.rename("pilt.jpg", "foto.jpg") |
Kausta loomine:
>>> os.mkdir("Teine kaust") |
Proovi kirjutada programm, mis loeb sisse kausta kõik failid ja väljastab nende sisu. Siin tuleb kasuks for-tsükkel. Katseta seda kaustaga, mis sisaldab tekstifaile.
Selle mooduliga saab ligi süsteemipõhisele funktsionaalsusele. Täisdokumentatsioon: https://docs.python.org/3/library/sys.html
Importimine:
>>> import sys |
Käsurea argumentidele saab ligi `sys.argv` muutujast:
>>> sys.argv |
Selline väärtus tuleb, kui käivitada Thonnys `programm.py` järgmise käsuga:
>>> % Run programm.py esimene teine |
Või käsurealt:
kasutaja@arvuti ~/Python/Moodulid$ python3 programm.py esimene teine |
Täisarvu maksimaalne väärtus praeguses arvutis:
>>> sys.maxsize |
Pythoni interpretaatori versiooni saamine:
>>> sys.version |
Programmi töö lõpetamine:
>>> sys.exit("Põhjus") |
Selle mooduli importimine väljastab Pythoni põhimõtted (The Zen of Python). Proovi seda importida.
Seda proovi ise importida. :)
Importisime palju erinevaid mooduleid ja kasutasime nende konstante ja funktsioone, aga kuidas need üldse töötavad? Kuidas luua ise üks moodul?
Mooduli importimine tegelikult otsib üles sellenimelise Pythoni faili ja käivitab selle. Kõiki mooduli failis defineeritud muutujaid saab importivas programmis kasutada.
Proovime ise mooduli teha. Kirjutame programmi minumoodul.py, kus defineerime ühe konstandi ja lihtsa funktsiooni. Lisame ka ühe print-lause, et tõestada, kuidas kogu programm käivitub.
konstant = "Tere" |
Nüüd avame samas kaustas interpretaatori või loome uue Pythoni faili ja proovime tehtud moodulit importida ning muutujaid kasutada.
>>> import minumoodul |
Kõik muutujad töötavad ning sõnum väljastatakse. Üldiselt välditakse moodulite importimisega millegi väljastamist. Teised moodulid ju seda ei teinud.
Mooduli kõikide muutujate loetlemiseks on olemas sisseehitatud funktsioon dir(). Kui sulud tühjaks jätta, näidatakse kõiki muutujaid, mis on kogu programmis, s.h imporditud moodulid. Kui sulgude sisse panna muutuja, siis loetletakse selle muutujaga seotud muutujaid.
>>> dir() |
Kui kunagi satub ette võõras muutuja, on mõistlik jooksutada selle peal dir() funktsioon, et näha kõiki selle võimalusi. Katseta ka help() funktsiooni.
Saame ka uurida erinevate objektide funktsioone ja muutujaid. Vaatame eelnevalt mainitud datetime objekti võimalusi.
>>> from datetime import datetime |
Leia vastused küsimustele:
Kirjutasime enda mooduli interpretaatoriga samasse kausta ja saime seda importida, aga muud moodulid ei asu selles kaustas ja neid saab ikka importida. Kus need asuvad?
Pythonil on erinevate moodulite asukohtadest nimekiri nimega path. Sellele saab ligi mooduli sys muutujast.
>>> import sys |
Mooduli importimisel otsitakse need kaustad järjest läbi, alustades esimesest. Esimene kaust on tühisõne ehk esimesena vaadatakse üle kaust, kus programm käivitati.
Imporditud moodulite failide asukohta saab ka näha:
>>> import turtle |
Mõned moodulid on Pythoni lähtekoodi sisseehitatud ja nende lähtekoodile nii lihtsalt ligi ei pääse.
>>> import math |
Proovi uurida moodulite this ja antigravity lähtekoode. Kas need vastavad ootustele?
Igal programmil (ja seega moodulil) on muutuja __name__ (kaks alakriipsu ees ja taga), mille väärtuseks on mooduli nimi sõnena. See muutuja on ka käivitataval programmil, aga selle väärtus on siis hoopis "__main__".
>>> __name__ |
Kui __name__ väärtuseks on mooduli nimi, siis järelikult programm imporditi. Kui selle väärtus on "__main__", siis see käivitati.
Tihti lisatakse programmi ette if-lause kontrolliga, kas programmi käivitati. Nii saavad teised arendajad programmi importida ja selle funktsioone kasutada ilma, et programmi põhiosa käivitub.
if __name__ == "__main__": |
Proovi seda järele minumoodul.py programmis.
1. Muuda minumoodul.py programmi nii, et see väljastab sõnumi ainult siis, kui see käivitatakse. Importimisel ei tohi midagi väljastada.
>>> %Run minumoodul.py |
2. Kirjuta programm, mis võtab käsurealt sisendiks päevade arvu ning väljastab kuupäeva ja aja pärast sisestatud päevade arvu.
Näide:
>>> %Run ajaarvutaja.py 1337 |
3. Google Mapsi koordinaatsüsteemi laiuskraadide piirid on -85 kuni +85 ning pikkuskraadide piirid on -180 kuni +180. Et minna Google Mapsi rakenduses mingitele koordinaatidele, saab minna veebilehitsejaga aadressile https://maps.google.com/?q=laiuskraad,pikkuskraad. Näiteks Delta hoone puhul https://maps.google.com/?q=58.385894,26.725829.
Kirjuta programm, mis genereerib juhusliku koordinaadipaari ja avab veebilehitsejaga selle asukoha Google Mapsis. Veebilehitseja peaks avama lehekülge samamoodi, nagu seda teeb antigravity moodul. Programmi koodi kirjuta kommentaar huvitava asukohaga, kuhu see sind viis.
4. Kirjuta programm, mis kasutab asjalikult kolme erineva mooduli funktsionaalsust. Võib kasutada mooduleid, mida materjalides ei ole käsitletud.
Õpiku teises peatükis räägiti enda arvuti failide lugemisest, aga alati ei pruugi vajalikud andmed olla enda failides olemas. Võibolla on vaja saada ajakohast infot muutuvate andmete kohta, näiteks ilmaennustused või valuutakursid. Võibolla on vaja midagi tõlkida Google Translate'iga, teha midagi Spotify andmetega, saada mingi sõna definitsioon või postitada midagi Twitterisse.
Sarnaselt lokaalsete failide lugemisega on võimalik lugeda veebilehti. Kõik veebilehed on justkui failid kellegi teise arvutis, mida veebilehitseja oskab graafiliseks liideseks teha. Suvaliste veebilehtede lugemine võib osutuda keeruliseks, aga Internetis on olemas palju selliseid lehti, mis on mõeldud programmidega lugemiseks. Neid nimetatakse rakendusliidestesks (ingl. k API ehk Application Programming Interface). Siin peatükis uurime, kuidas avada veebilehti nagu faile ja saada rakendusliidestelt andmeid.
Enne jätkamist tuleb paigaldada moodul requests. Seda saab teha käsuga pip install requests. Katsetamiseks kirjuta Pythonis import requests. Kui erindit ei visata, on moodul paigaldatud. Kui ei ole kindel, kuidas mooduleid installida, siis on õpikus moodulite paigaldamise juhised.
Sellest peatükist arusaamiseks on vaja läbida õpiku esimesed 2 peatükki ning tutvuda 10. peatüki sõnastiku andmestruktuuriga.
Pythoniga veebilehtede lugemise teeb väga lihtsaks moodul requests ja selle funktsioon get(), mis tagastab päringu objekti. Selle päringu lähtekoodi sõnena saab kätte väljagae text. Avame näiteks Tartu Ülikooli veebilehe ja väljastame selle lähtekoodi.
>>> import requests |
Väljastatud HTML-kood ei ole kasutajale ega programmile hästi loetav. Pythoniga on võimalik HTML-koodi parsida kasutades mooduleid BeautifulSoup või lxml, aga siin me HTML-iga rohkem ei tegele. Vaatame hoopis rakendusliideseid, mis teevad programmidele andmete kättesaamise lihtsaks.
Et teha andmed programmile kergesti loetavaks, kasutavad paljud rakendusliidesed JSON-vormingut. See vorming lubab sõnedes hoida JavaScripti objekte (sealt nimi JavaScript Object Notation), mis on väga sarnased Pythoni sõnastikega.
Üks JSON objekt näeb välja selline:
tudeng = { |
Requests moodul oskab sellised leheküljed lugeda Pythoni sõnastikeks meetodiga json().
Näiteks saab tudengi nime saab kätte koodiga tudeng["nimi"] ja esimese hobi saab kätte koodiga tudeng["hobid"][0]. Väärtusteks võib olla ka teine JSON-objekt ehk sõnastik. Milline näeks tudeng välja siis, kui võtme "hinded" all oleks sõnastik hinnetega? Kuidas siis saada kätte aine "Programmeerimine" hinne?
Leiame ühe rakendusliidese, mis tagastab JSON-vormingus informatsiooni ja proovime seda lugeda. Üks selline on näiteks https://exchangeratesapi.io/, mis annab meile praegused valuutakursid. Proovime kätte saada EUR/USD kursi.
>>> import requests |
Tihti on vaja päringuid täpsustada. Näiteks eelmises näites tagastati meile kõikide valuutade väärtused euro suhtes, aga kuidas täpsustada baasvaluutat? Nagu dokumentatsioon meile ütleb, tuleb pärida aadressi https://api.exchangeratesapi.io/latest?base=USD. See on tegelikult sama aadress mis enne, aga lisatud on parameeter "base" väärtusega "USD".
Parameetreid saab manuaalselt aadressi taha küsimärgiga lisada, nagu on näidisaadressilt näha. Lihtsam on lisada parameetrite sõnastik päringule kaasa. Eriti siis, kui peab lisama mitu parameetrit või kui parameetrite väärtused sisaldavad sümboleid.
>>> import requests |
Proovi teha päring valuutakursside ajaloole: https://api.exchangeratesapi.io/history. Vaata dokumentatsioonist, millised parameetrid juurde lisama peab.
Siiani oleme veebilehtede lugemisel tegelikult taustal teinud GET-päringuid. See on lihtsalt päring, mis annab veebilehele teada, et me tahame saada selle sisu. Neid kasutatakse siis, kui me ainult pärime ja tulemusena midagi ei muudeta. Kui serveris midagi muudetakse, kasutatakse POST-päringuid. POST-päringutega saadetakse veebilehtedele andmeid ilma, et neid aadressi taha lisatakse. Ka nende päringutega saadetakse üldiselt midagi vastu.
Lühendame näiteks internetiaadressi, kasutades teenust https://rel.ink/. Selle käigus tehakse serverisse kirje, et pikale aadressile vastab uus väiksem aadress, mille tõttu peab tegema POST-päringu. Rakendusliidese dokumentatsioonis on kirjas, et peab tegema POST-päringu aadressile https://rel.ink/api/links/ ja andma kaasa "url" parameetriga aadressi, mida tahame lühendada.
>>> import requests |
https://rel.ink/9mvm2g viib nüüd aadressile https://progeopik.cs.ut.ee/.
Internetis leidub palju rakendusliideseid, mis tagastavad JSON-vormingus andmeid. Mõned näited:
Paljud rakendusliidesed ei ole avalikult kättesaadavad ning nõuavad autentimist, et päringuid piirata. Nende kasutamiseks peab registreerima kasutajaks ning iga päringuga kaasa saatma kasutajaga seotud võtme.
Mõned näited autentimisega rakendusliidestest:
Sellistele teenustele tehakse tavaliselt eraldi Pythoni moodulid, et neid oleks kergem kasutada:
Mahukad nimekirjad rakendusliidestest:
1. Kirjuta valuutakalkulaator, kuhu sisestatakse nii baasvaluuta, soovitud valuuta ja raha kogus.
Näide:
>>> %Run valuutaarvutaja.py Sisesta baasvaluuta: GBP Sisesta teine valuuta: IDR Sisesta kogus: 1 1 GBP on väärt 18262.95 IDR |
2. Kirjuta programm, mis küsib kasutajalt sisendit, kasutab mõnda huvitavat rakendusliidest ning väljastab vastavalt sisendile rakendusliidesest saadud infot.
Paljud programmeerijatel ette tulevad ülesanded on seotud sõnade ja teksti analüüsiga. Sellised ülesanded võivad osutuda üpris keeruliseks. Näiteks, kuidas teha kindlaks, et kasutaja sisestas ikka korrektse e-posti aadresi? Kuidas leida üles tekstist kõik telefoninumbrid? Kuidas[1] eemaldada[5] Vikipeedia[15] artiklist[43] kõik[119] viited[327]? Neid ülesandeid võib ju proovida puhta Pythoniga lahendama hakata, aga selliseid ja keerukamaidki ülesandied on palju lihtsam ja kiirem lahendada regulaaravaldistega.
Selle peatüki läbimiseks piisab Pythoni installatsioonist.
Et peatükist aru saada, peab läbima õpiku esimesed 3 peatükki ja tutvuma järjenditega 7. peatükist.
Regulaaravaldis on lihtsalt muster erinevatest sümbolitest, mille abil sõnede osasid otsida.
Kõige lihtsam regulaaravaldise muster oleks mingi sõna ise. Näiteks mustriga "koer" saab teha kindlaks, kas sõne algab osasõnega "koer", leida sõnest kõik osasõne "koer" esinemised või asendada sõnes kõik osasõned "koer" millegi muuga.
Selliseid ülesandeid on muidugi lihtne täita ka Pythonisse sisseehitatud funktsioonidega, vastavalt sõne meetodid startswith(), find() ja replace(). Ülesanne muutub keeruliseks, kui otsitav muster ei ole lihtsalt mingi sõne. Näiteks, kuidas leiame kõik neljatähelised sõnad, mis algavad k-tähega ja lõppevad r-tähega?
Suvalisi sümboleid saab regulaaravaldistes märkida punktiga. Eelmises lõigus esitatud küsimuse vastuseks sobib muster "k..r".
Regulaaravaldisi saab katsetada lehekülgedel https://regexr.com/ ja https://regex101.com. Ülemisse tekstikasti kirjuta muster ja alumisse kirjuta tekste, millega mustrit katsetada. Mustrile vastavad osad värvitakse ära ning nende peale vajutades saab lisainfot. Katseta edaspidi kõik regulaaravaldiste oskused nende rakendustega läbi.
Saime teada, et punktidega tähistatakse ükskõik mis sümboleid. Kui meile sobivad ainult mõned sümbolid, kasutame nurksulgi. Nende sisse lähevad sümbolid, mida otsime.
Sümbol | Tähendus |
. | Sobivad kõik sümbolid. |
[abc] | Sobivad sümbolid a, b ja c. Nurksulgude vahele võib kirjutada mistahes sümboleid. |
[a-z] | Sobivad tähed a kuni z ladina tähestikus. |
[A-Za-z0-9] | Sobivad tähed A kuni Z, a kuni z ja numbrid 0-9. |
\w | Sobivad kõik numbrid ja tähed (k.a täpitähed) ja alakriipsud. |
\d | Kõik arvud. Sama, mis [0-9]. |
Näited:
Muster | Sobivad | Ei sobi |
koer | koer | kõik muu |
k..r | koer, kaer, koor, k03r, ... | koger, kass, kor, kr, ... |
[ktpb]ass | kass, tass, pass, bass | kõik muu |
[0-9][0-9][0-9][A-Z][A-Z][A-Z] | 123ABC, 420BLZ, 111YKS jms tavalised auto numbrimärgid | kõik, mis ei ole tavalised auto numbrimärgid |
\w\w\w\w\w\w | kõik 6-tähemärgilised sõnad, mis sisaldavad tähti, numbreid ja alakriipse | kõik muu |
Erinevate tähtede võimalusi sai määrata nurksulgudega. Pikemaid võimalusi tuleb eraldada püstkriipsudega ja vajadusel paigutada sulgude sisse.
Sümbol | Tähendus |
| | Või-märk. Sobib vasakule poole jääv grupp või paremale poole jääv grupp. |
() | Grupeerimismärgid. Grupid tuleb ümbritseda sulgudega. |
Näited:
Muster | Sobivad | Ei sobi |
koer|kass | koer, kass | kõik muu |
k(oer|ass) | koer, kass | kõik muu |
k(o|a)er | koer, kaer | kõik muu |
(ko|ka)(er|ss) | koer, kass, kaer, koss | kõik muu |
(je|bo|ka)ss | jess, boss, kass | kõik muu |
aias sadas (sai|leib)a | aias sadas saia, aias sadas leiba | kõik muu |
Mustris saab veel ära määrata, et mõni sümbol või grupp saab olla sõnes mitu korda.
Sümbol | Tähendus |
? | Eelnev sümbol/grupp kas on või ei ole. |
* | Eelnevat sümbolit/gruppi on null või rohkem kordi. |
+ | Eelnevat sümbolit/gruppi on üks või rohkem kordi. |
{5} | Eelnevat sümbolit/gruppi on täpselt 5 korda. |
{5,} | Eelnevat sümbolit/gruppi on 5 või rohkem kordi. |
{1,5} | Eelnevat sümbolit/gruppi on 1 kuni 5 korda (1 ja 5 kaasa arvatud). |
Näited:
Muster | Sobivad | Ei sobi |
jaa? | ja, jaa | kõik muu |
(mitte )?olla | olla, mitte olla | kõik muu |
ja* | j, ja, jaa, jaaa, ... | kõik muu |
ja(ja)* | ja, jaja, jajaja, ... | kõik muu |
ja+ | ja, jaa, jaaa, jaaaa, ... | kõik muu |
ja{2,4} | jaa, jaaa, jaaaa | kõik muu |
[0-9]{3}[A-Z]{3} | tavalised auto numbrimärgid | mitte tavalised auto numbrimärgid |
\w{6} | kõik 6-tähemärgilised sõnad, mis sisaldavad tähti, numbreid ja alakriipse | kõik muu |
.+ | kõik sõned, kus on vähemalt midagi | tühisõne |
Et täpsustada sõne algust ja lõppu, on olemas märgid ^ ja $.
Sümbol | Tähendus |
^ | Sõne algus. |
$ | Sõne lõpp. |
Näited:
Muster | Sobivad | Ei sobi |
^a | aabits..., arvuti..., algebra..., a123..., a... | muude sümbolitega algavad sõned |
ass$ | ...kass, ...bass, ...kontrabass, ... | muude sümbolitega lõppevad sõned |
^koer$ | koer | kõik muu |
Oleme vaadanud palju erisümboleid, millega regulaaravaldise mustreid koostada:
. | [ | ] | \ | | | ( | ) | ? | * | + | { | } | ^ | $ |
Aga kuidas neid sümboleid sõnedes otsida? Samamoodi, nagu Pythonis tehakse sõne sees jutumärke, saab ka regulaaravaldistes teha erisümboleid: nende ette tuleb panna langkriips. Inglise keeles öeldakse selle kohta escaping. Lühidalt: et otsida punkti, peab otsima "\.", küsimärgi otsimiseks peab otsima "\?", plussi otsimiseks "\+" jne.
Muster | Sobivad | Ei sobi |
\.\.\. | ... | kõik muu |
youtube\.com | youtube.com | kõik muu |
\(.*\) | (kass), (koer), (abc123), ... | mitte sulgudes olevad sõned |
\[\d+\] | [1], [42], [1632], ... | [], mitte nurksulgude vahel olevad arvud |
¯\\_\(ツ\)_\/¯ | ¯\_(ツ)_/¯ | kõik muu |
Regulaaravaldisi saab harjutada leheküljel RegexOne. Proovi ka regulaaravaldiste ristsõnu või golfi.
Kasutame regulaaravaldisi lõpuks praktikas. Et Pythonis regulaaravaldisi kasutada, tuleb importida moodul re. Selle nimi on lühend ingliskeelsest terminist regular expressions.
>>> import re |
Mustreid defineeritakse justkui sõnesid, aga jutumärkide ette tuleb panna r-täht. See lubab sõnedes kasutada langkriipse ilma teise langkriipsuta.
>>> muster = r"\[\d+\]" |
Uurime, kas muster "k..r" vastab erinevatele sõnedele. Seda saab teha funktsiooni re.match() abil. See funktsioon tagastab objekti, kui sõne vastab mustrile. Vastasel juhul ei tagasta see midagi.
>>> muster = r"k..r" |
Tegelikult kontrollib re.match() ainult, kas muster vastab sõne algusele. Et kontrollida tervet sõne, on mõistlik ette panna ^ ja taha $.
>>> re.match(r"k..r", "koerad") |
Seda saab ära kasutada if-lausetes:
import re |
Millised kasutajanimed programmile meeldivad? Proovi erinevaid variante.
Sarnast kontrolli saab teha e-posti aadressidega, aga muster on keerulisem:
# https://emailregex.com/ |
Proovi muuta programmi nii, et kontrollitakse telefoninumbri korrektsust.
Funktsioon re.findall() leiab üles tekstist kõik mustri vastavused:
>>> tekst = "koer kass kaer koor koger k03r k??r" |
Proovi leida tekstist erinevaid mustreid: auto numbrimärgid, kasutajanimed, e-posti aadressid, telefoninumbrid.
Funktisooniga re.sub() (sõnast substitute) saab asendada tekstis kõik mustri vastavused:
>>> tekst = "koer kass kaer koor koger k03r k??r" |
Selle koodiga asendasime kõik mustrile "k..r" vastavad osasõned sõnega "kass".
Kopeerisid Vikipeediast pika artikli, aga teksti sisse jäävad nurksulgudes viited.
>>> tekst = "Python was conceived in the late 1980s[34] by Guido van Rossum at Centrum Wiskunde & Informatica (CWI) in the Netherlands as a successor to the ABC language (itself inspired by SETL),[35] capable of exception handling and interfacing with the Amoeba operating system.[8] Its implementation began in December 1989.[36]" |
Nende eemaldamine manuaalselt on tüütu ja neid koodiga eemaldada võib esmapilgul tunduda keeruline, aga regulaaravaldised teevad selle väga lihtsaks:
>>> muster = r"\[\d+\]" |
Regulaaravaldistega on muidugi võimalik palju rohkem teha. Sellel kursusel rohkemat ei käsitleta. Regulaaravaldisi vaadatakse põhjalikumalt aines "Automaadid, keeled ja translaatorid" (LTAT.03.006).
Üks kasulik mainimata jäänud regulaaravaldise võimalus on gruppidest väärtuste saamine.
Tasub ka uurida mooduli re dokumentatsiooni: https://docs.python.org/3/library/re.html
1. Kirjuta programm numbrileidja.py, mis leiab failist üles kõik telefoninumbrid järgmise vorminguga:
Mõne telefoninumbri näited:
Näiteks kontakt.txt sisu (andmed pärit Riigikogu leheküljelt):
Riigikogu Henn Põlluaas +372 631 6301 henn.polluaas@riigikogu.ee Helir-Valdor Seeder +3726316311 helir-valdor.seeder@riigikogu.ee Siim Kallas +372 6316321 siim.kallas@riigikogu.ee Tiiu Pohl 6316302 tiiu.pohl@riigikogu.ee |
numbrileidja.py käivitamine:
Sisesta failinimi: kontakt.txt Leitud telefoninumbrid: +372 631 6301 +3726316311 +372 6316321 6316302 |
Siiani oleme andmeid lugenud ja salvestanud tekstifailidesse. Väheste andmetega on see lihtne ja kiire lahendus, aga mida rohkem andmeid koguneb, seda aeglasemaks läheb nende otsimine. Mida keerulisemaks lähemad andmestruktuurid, seda keerulisem on neid failides hoida. Kõigi selle lahendamiseks on välja töötatud andmebaasisüsteemid, mis lubavad hoida palju keerulisi andmeid organiseeritult nii, et neile pääseb kiiresti ligi.
Mõned sellised süsteemid on MySQL, PostgreSQL, MongoDB, Oracle Database ja Sybase SQL Anywhere. Siin õppematerjalides keskendume hoopis süsteemile SQLite. See on üks lihtsamatest süsteemidest, mida kasutavad tihti rakendused andmete hoidmiseks. Seda kasutavad näiteks Apple, Google, Facebook, Firefox ja paljud muud. Kõige tähtsam: see on Pythonisse sisse ehitatud!
Jätkamiseks ei pea paigaldama lisatarkvara. Võibolla tuleb kasuks SQLite Browser.
Et peatükist aru saada, peab läbima õpiku esimesed 3 peatükki ning tutvuma 7. peatüki järjendite ja ennikutega.
Paljudel eelmainitud andmebaasi süsteemidel on midagi ühist: need kasutavad SQL-i. SQL (Structured Query Language) on päringukeel, mida kasutatakse andmebaaside loomiseks, andmete kirjutamiseks, pärimiseks ja muutmiseks ning paljuks muuks.
SQLite'i saab katsetada lehel SQLite Online. Kõik edaspidised käsud tasub sinna kirjutada ja läbi proovida.
Enne, kui saame andmeid andmebaasi salvestada, peame looma tabeli, mis kirjeldab andmete struktuuri.
Oletame, et meil on veebileht, kus saab kasutajaks registreerida ja sisse logida. Igal kasutajal on mingi järjenumber ehk ID, tekstiline kasutajanimi ja tekstiline parool. Selliseid omadusi nimetatakse väljadeks.
Loome tabeli selliste väljadega ja paneme selle nimeks "Kasutajad". Tabelit saab luua käsuga CREATE TABLE Tabel (...) ning sulgudesse lähevad väljade nimed ja nende tüübid:
CREATE TABLE Kasutajad (id INTEGER PRIMARY KEY, kasutajanimi TEXT, parool TEXT); |
Kõik käsud tuleb lõpetada semikooloniga.
PRIMARY KEY tähendab seda, et see on põhiline väli, millega sellele kasutajale viidata saab ning et iga järgmine kasutaja saab ühe võrra suurema järjenumbri.
Nüüd on tabel olemas ja puudu on ainult andmed. Andmeid saab tabelisse lisada käsuga INSERT INTO Tabel (...) VALUES (...) nii, et esimestesse sulgudesse lähevad väljade nimed ja teistesse lähevad nende väärtused:
INSERT INTO Kasutajad (kasutajanimi, parool) VALUES ('admin', 'parool123'); |
Väljale ID ei pea väärtust omistama, sest siis määratakse selle väärtuseks automaatselt alguses 1, edaspidi tuleb selle väärtus eelmisest ühe võrra suurem. See on PRIMARY KEY tõttu. Sõned peavad olema ümbritsetud ülakomadega, mitte jutumärkidega.
Lisame veel mõned kasutajad:
INSERT INTO Kasutajad (kasutajanimi, parool) VALUES ('kasutaja', 'hunter2'); |
NB! Paroole ei tohi kunagi salvestada tavalise tekstina. Neid tuleks räsida ühepoolse algoritmiga nagu SHA-256. Näiteks tuleb "parool123" asemel salvestada andmebaasi räsi "f9c80861456cdd34bebfa8886ae3436f22bbc7343e27df6a3376bcce23ed330d". Parooli kaudu on võimalik saada räsi, aga räsi kaudu ei ole võimalik saada parooli. Kui keegi tahab sisse logida, tuleb sisselogimisparool ka räsida ja seejärel räsisid võrrelda. Kui räsid on võrdsed, siis sisestatud parool on õige. Räsimine on tähtis, sest kui keegi kunagi andmebaasile ligi saab, ei saada kätte paroole. Pythonis saab räsisid arvutada näiteks bcrypt mooduliga.
Kui andmed on tabelisse lisatud, peab neid kuidagi kätte saama. Seda tehakse SELECT käsuga. Et kuvada kõik tabeli read, saab kirjutada käsu:
SELECT * FROM Kasutajad; |
id | kasutajanimi | parool |
1 | admin | parool123 |
2 | kasutaja | hunter2 |
3 | hea_nimi | hea_parool |
Tärn tähendab seda, et tagastatakse kõik väljad. Saab ka valida, milliseid tagastada:
SELECT id, kasutajanimi FROM Kasutajad; |
id | kasutajanimi |
1 | admin |
2 | kasutaja |
3 | hea_nimi |
Saame täpsustada, milliseid andmeid tahame võtmesõnaga WHERE:
SELECT parool FROM Kasutajad WHERE kasutajanimi='admin'; |
parool | |
parool123 |
Kui on vaja muuta näiteks kasutaja parooli, tuleb teha UPDATE käsk.
UPDATE Kasutajad SET parool='hunter3' WHERE kasutajanimi='kasutaja'; |
Tabelit saab kustutada käsuga DROP TABLE. Selle käigus kustutatakse ka kõik read, seega selle käsuga peab olema ettevaatlik.
DROP TABLE Kasutajad; |
SQL-il on veel palju rohkem võimalusi nagu mitme tabeli omavahelised suhted, vaated, funktsioonid jne. Siin kursusel keerulisemat SQL-i ei käsitleta. Andmebaasidesse ja SQL-i süvenetakse täies rauas aines "Andmebaasid" (LTAT.03.004). Enne seda saab SQL-i harjutada ja edasi õppida SQLZOO leheküljel.
Kõige põhilisemad SQL-käsud on nüüd selged. Proovime teha samasuguse andmebaasi Pythonis. Nagu eelnevalt mainitud, on SQLite Pythonisse sisseehitatud. Selle kasutamiseks tuleb importida moodul sqlite3.
>>> import sqlite3 |
Mooduli dokumentatsioon: https://docs.python.org/3/library/sqlite3.html
Et andmebaasiga suhelda, tuleb kõigepealt luua ühendus. See on sarnane faili avamisele. Ühtlasi hoitakse SQLite andmebaase failides. Teiste andmebaasisüsteemidega on tavaliselt keerulisem ühendada.
>>> ühendus = sqlite3.connect("andmebaas.db") |
Kui sellise failinimega andmebaasi veel pole, siis see luuakse automaatselt.
Et andmebaasi käske saata, peab veel tegema kursori, mille kaudu seda teha.
>>> kursor = ühendus.cursor() |
Nüüd saab saata SQL-käske execute() meetodiga. Loome kõigepealt sama tabeli, mille lõime enne.
>>> käsk = "CREATE TABLE Kasutajad (id INTEGER PRIMARY KEY, kasutajanimi TEXT UNIQUE, parool TEXT);" |
Veel ei ole tegelikult andmebaasis ühtegi muudatust tehtud. Kui käivitatakse käsud, mis andmebaasis midagi muudavad, on vaja need ka kinnitada.
>>> ühendus.commit() |
Sarnaselt failidega tuleb andmebaasi ühendus pärast kasutamist kinni panna:
>>> ühendus.close() |
Kui ühendus on avatud, ei saa teised programmid samal ajal sama andmebaasiga ühendada: visatakse erind, et andmebaas on lukus.
Loome uuesti ühenduse andmebaasiga.
Andmete lisamiseks saab muidugi lihtsalt teha kursor.execute() nagu enne ja sisestada INSERT käsk, aga siin tuleb väga ettevaatlik olla, sest andmebaasi lisatavad andmed tulevad tihti kasutajatelt.
Oletame, et saame kasutajalt kasutajanime ja parooli ning meie käsk on järgmine:
>>> käsk = "INSERT INTO Kasutajad (kasutajanimi, parool) VALUES ('" + kasutajanimi + "', '" + parool + "');" |
Kui sisestatud kasutajanimi ja parool on tavalised, siis on käsk korralik:
>>> käsk |
Aga mis siis, kui muutuja kasutajanimi saab sellise väärtuse?
>>> kasutajanimi = "'; DROP TABLE Kasutajad;" |
Siis terve käsk oleks selline:
>>> käsk |
INSERT käsk lõpetatakse varakult ära, sest kasutajanimi lõpetas ülakomaga sõne ära ja semikooloniga. See ei ole korrektne käsk, sest üks muutuja on puudu ja seda ignoreeritakse. Siis tehakse käsk DROP TABLE Kasutajad, mis kustutab terve tabeli ära. Lõpus on mõned sümbolid, mis ei tee ka midagi. Kasutajal õnnestus andmebaasile ligipääsuta kustutada ära terve tabel.
Sellist manöövrit kutsutakse SQL-i süstimiseks (ingl. k. SQL injection) ja see on väga levinud rünnak rakenduste vastu:
Selle vältimiseks on sqlite3 moodulil võimalused olemas: muutujad tuleb asendada küsimärkidega ja nende väärtused lähevad ennikuna teise parameetrisse.
>>> kasutajanimi = "admin" |
Nii ei lõpeta ülakoma sõne ja kasutajanimeks saab päriselt see, mis on muutuja väärtus.
NB! Ühe elemendiga ennik on Pythonis vormingus (muutuja,). Lihtsalt sulgudes muutuja on sama, mis muutuja ise.
Lisa ka teised kasutajad tabelisse. Et muutused sisse läheks, peab jälle ühenduse kinnitama.
>>> ühendus.commit() |
SELECT käskude tulemused saab kätte kursorist fetchall() meetodiga.
>>> kursor = ühendus.cursor() |
Tagastatakse järjend ennikutest, mis sisaldavad iga rea andmeid soovitud järjekorras. Kui järjekorda pole täpsustatud ja on kasutatud tärni, võetakse järjekord tabeli loomise käsust: id, kasutajanimi, parool.
Siin kursusel rohkem andmebaase ei käsitleta. Kui sarnaselt jätkata sqlite3 mooduliga, võivad mahukad projektid minna liiga keeruliseks. Selle vastu on loodud moodulid nagu SQLAlchemy ja Peewee, mis viivad vastavusse andmebaasi tabelid ja Pythoni objektid (ingl. k. Object Relational Mapping). See teeb andmebaasiga suhtlemise koodi loetavamaks ning SQL-i ei pea kirjutamagi. Pythoni objektidest räägitakse lähemalt peatükis "Objektorienteeritud programmeerimine".
1. Kirjuta programm loo_andmebaas.py, mis loob tabeli, millel on 4 välja:
2. Kirjuta programm registreeri.py, millega luuakse eelmises ülesandes loodud tabelisse uus kasutaja.
Sisesta kasutaja: admin Sisesta parool: parimparoolmaailmas Sisesta lemmik arv: 1337 Kasutaja loodud! |
Sisesta kasutaja: admin Kasutajanimi on juba võetud! |
3. Kirjuta programm logisisse.py, milles saab sisse logida eelmises ülesandes loodud kasutajatesse.
Sisesta kasutaja: admin2 Kasutajat ei leitud! Sisesta kasutaja: admin Sisesta parool: kõigeparemparoolmaailmas Vale parool! |
Sisesta kasutaja: admin Sisesta parool: parimparoolmaailmas Edukalt sisse logitud! Sinu lemmik arv on 1337. |
Kindlasti kasutad igapäevaselt veebilehti nagu Google, YouTube, Facebook, Instagram, Twitter, Vikipeedia või vähemalt oled neist kuulnud. Need on kõik tegelikult veebirakendused, mille taga jookseb mingi kood, mis saadab veebilehitsejate päringutele andmeid vastu. Uurime, kuidas luua enda lihtne veebirakendus ja kuidas seda Internetti püsti panna.
Veebirakendusi kirjutatakse keeltega PHP, Java, Ruby ja muidugi ka Python. Veebirakenduste jaoks on Pythonil palju erinevaid raamistikke: Django, TurboGears, Pyramid, Flask ja väga palju muid. Selles peatükis keskendume Flaskile, mis lubab veebirakenduse püsti saada vaid mõne reaga.
Enne jätkamist tuleb paigaldada moodul flask. Seda saab teha käsuga pip install flask. Katsetamiseks kirjuta Pythonis import flask. Kui erindit ei visata, on moodul paigaldatud. Kui ei ole kindel, kuidas mooduleid installida, siis on õpikus moodulite paigaldamise juhised.
Et peatükist aru saada, peab olema läbitud õpiku esimene osa. Kasuks tuleb HTML-i tundmine, aga selle peatüki läbimiseks ei pea seda oskama. HTML-i õppematerjalid: eesti keeles, inglise keeles.
Flask on Pythoni moodul lihtsate veebirakenduste loomiseks, mis keskendub kiiresti alustamisele ja lihtsusele. Selle lihtsus ei takista ka suuremate veebirakenduste loomist. Seda kasutavad teiste raamistike kõrval näiteks Netflix, Airbnb, Reddit, Uber ja muud. Selles peatükis kirjutame lihtsa veebirakenduse, mis võtab kasutajalt sisendit ja arvutab selle põhjal tulemuse. Vaatame ka, kuidas seda Internetti üles laadida ja teistele jagada.
Paneme kõigepealt püsti kõige lihtsama võimaliku veebirakenduse: üksainus lehekülg, mis tagastab mingit teksti. Salvesta järgnev kood faili teremaailm.py.
from flask import Flask |
Proovi seda programmi jooksutada. Peaks ilmuma selline väljund:
* Serving Flask app "teremaailm" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 123-456-789 |
Koodi jooksutamine käivitas arvutis veebiserveri aadressil http://127.0.0.1:5000/. 127.0.0.1 on IP aadress, mis tähendab praeguse arvuti võrku ja 5000 tähendab porti, mille peal server jookseb praeguse arvuti võrgus. Nüüd proovi minna veebilehitsejas sinna aadressile ja seal peaks olema tekst "Tere, maailm!":
Pärast külastamist ilmub konsooli ka selline väljund:
127.0.0.1 - - [06/Apr/2020 14:24:47] "GET / HTTP/1.1" 200 - |
See tähendab, et lehekülge "/" külastati sellel kellaajal IP-aadressilt 127.0.0.1.
Kui programm ei käivitunud õigesti, siis Flaski dokumentatsioon pakub välja erinevaid lahendusi: https://flask.palletsprojects.com/en/1.1.x/quickstart/#what-to-do-if-the-server-does-not-start
Käime rakenduse ridahaaval läbi.
from flask import Flask |
Impordime Flaski rakenduse klassi ja salvestame sellest isendi muutujasse app. Parameeter __name__ annab Flaskile teada, kuidas programmi käivitati.
@app.route("/") |
See on dekoraator, mis ütleb rakendusele, et järgmine funktsioon kutsutakse välja, kui minnakse aadressile "/". Kaldkriips tähendab juurt ehk antud juhul http://127.0.0.1:5000/. "/tere" vastaks aadressile http://127.0.0.1:5000/tere. Dekoraator teeb põhimõtteliselt seda, et sellele järgnevat funktsiooni kasutatakse ühes teises funktsioonis, aga sellest ei ole Flaski kasutamisel vaja aru saada.
def index(): |
See funktsioon määrab ära, mida veebirakendus teeb, kui minnakse dekoraatoris määratud leheküljele. Funktsiooni tagastus saadetakse lehe külastajale. Praegu tagastatakse lihtsalt tekst "Tere, maailm!", aga tavaliselt laaditakse kuskilt andmeid, tehakse arvutusi ja tagastatakse HTML-kood, mille veebilehitseja teeb ilusaks veebileheks. Siin saab ka vastu võtta kasutaja sisendeid ja nende põhjal muuta tagastust. Funktsiooni nimi ei ole praegu tähtis.
if __name__ == "__main__": |
Siin käivitatakse rakendus, kui programm käivitati, mitte ei imporditud.
Debug tähendab, et kui lehekülje laadimisel juhtub viga, siis seda kuvatakse veebilehitsejas, mitte ainult konsoolis. See teeb ka seda, et kui programmi koodi muudetakse, siis programmi ei pea uuesti jooksutama, vaid see taaskäivitatakse automaatselt.
Proovi muuta väärtust, mida index() funktsioon tagastab ja vaata, mis konsooli ilmub. Ilma programmi taaskäivitamiseta, külasta veebilehitsejaga aadressi uuesti. Kas veebilehel on uus tekst?
Esimeses näites tegime ühe lehekülje aadressile "/". Teeme teise lehekülje teisele aadressile:
@app.route("/teine") |
Ühel leheküljel võib olla mitu aadressi:
@app.route("/teine") |
Aadressid võivad olla isegi dünaamilised, s.t aadressi sees on muutuja. Selleks tuleb aadressis teravnurksulgude vahele panna muutujanimi ning sama muutuja panna funktsiooni parameetritesse. Seda muutujat saab nüüd funktsioonis kasutada.
@app.route("/leht/<arv>") |
Katseta erinevaid aadresse. Leia vastused küsimustele:
Lehekülje sisu ei pea samuti staatiline olema. Oletame, et lehekülg random.org on maas ja kirjutame sellest enda versiooni. Algul tagastame lihtsalt juhusliku arvu 1-st 10-ni:
from flask import Flask |
Proovi täiendada seda rakendust nii, et kasutaja saab ise määrata juhusliku arvu ülem- ja alampiiri.
Kui külastada lehekülje juurt ehk "/", on piirid 1 ja 10.
Kui külastada aadressi "/<maksimum>", on piirid 1 ja <maksimum>.
Kui külastada aadressi "/<miinimum>/<maksimum>", on piirid <miinimum> ja <maksimum>. Seda peaks lahendama ainult ühe funktsiooniga.
Vihje: tuleta meelde, kuidas määrata funktsiooni parameetritele vaikeväärtused.
Eelmise ülesandega tegime rakenduse, millel on kaks parameetrit: ülem- ja alampiir. On kolm võimalust neid sisestada: mitte ükski; ainult ülempiir; nii alampiir kui ülempiir. Dekoraatorid tulevad sellised:
@app.route("/") |
Aga mis siis, kui tahame, et kasutaja sisestab ainult alampiiri ning ülempiir saab vaikeväärtuse? Mis siis, kui meil on palju parameetreid ja neil on kõigil mingid vaikeväärtused? Saaksime teha mingi väärtuse, mille puhul võetakse vaikeväärtus, praegu näiteks "/<miinimum>/default", aga see ei ole väga ilus lahendus. Selle jaoks on olemas võimalus lisada aadressidele parameetrid.
Vaatame YouTube'i video linki: https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=43
Selle aadress on tegelikult lihtsalt https://www.youtube.com/watch, aga sellel on kaasas kaks parameetrit:
{ |
Flaskis saab parameetrite väärtustele ligi requests.args muutuja kaudu, mille peab flask moodulist importima. Väärtused saab kätte get() meetodiga. Neile saab ka määrata vaikeväärtuse ja tüübi. Sama lehekülg oleks Flaskis pool-pseudokoodina umbes selline:
from flask import Flask, request, redirect |
Mida YouTube video_id-ga teeb, on ärisaladus. Muutuja start_time tehakse automaatselt täisarvuks ja selle vaikeväärtus on 0.
Proovi lisada oma juhusliku arvu generaatorile võimalus valida alam- ja ülempiirid aadressiparameetritega. Kasuta YouTube'i näidet, et parameetrite väärtused kätte saada.
Kui külastada lehekülje juurt ilma parameetriteta ehk aadressi "/", on piirid 1 ja 10.
Kui külastada aadressi "/?maksimum=20", on piirid 1 ja 20.
Kui külastada aadressi "/?maksimum=20&miinimum=10", on piirid 10 ja 20.
Siiani oleme tagastanud ainult teksti, aga leheküljed ei ole ju tavaliselt sellised. Lehekülgedel on struktuuri, värve, suuri tekste, väikeseid tekste, pilte, linke. Et anda leheküljele struktuur, tuleb kasutada HTML-koodi. Ilusaks saab seda teha CSS-ga. Selle peatüki eesmärk ei ole neist kumbagi õpetada. Kasutame olemasolevat disaini ja vaatame, kuidas Flaskiga seda tagastada.
Flask nõuab, et lehekülje failid on spetsiifilistes kaustades. HTML-failid lähevad kausta templates/ ning CSS-failid, pildid ja muud staatilised failid lähevad kausta static/. Failipuu peaks tulema järgmine, kui app.py on Pythoni fail, kus käivitatakse Flaski rakendus:
templates/ ├─index.html └─about.html static/ ├─style.css ├─pilt.jpg └─script.js app.py |
Salvesta järgmine HTML-kood faili index.html ja paiguta see kausta templates/.
<!DOCTYPE html> |
Salvesta järgmine CSS-kood faili style.css ja paiguta see kausta static/.
body { |
Nüüd on vaja Flaskiga HTML tagastada. Seda saab teha funktsiooniga render_template(). Sulgudesse läheb HTML-faili nimi.
from flask import Flask, render_template |
Kui HTML- ja CSS-fail on õigesti paigutatud, tuleb lehekülg selline:
Lehekülg näeb nüüd välja nagu lehekülg. Muidugi, genereeritav arv on alati 3 ja nupule vajutamine seda ei muuda. Järgmisena tuleb see tööle saada.
Kui vajutada nupu "Genereeri" peale, siis teeb veebilehitseja leheküljele värskenduse ja aadressi lõppu lisatakse juba parameetrid sisendikastidelt:
127.0.0.1:5000/?miinimum=1&maksimum=10 |
Need tulevad tänu sellele, et <form> elemendil on atribuudid action="." ja method="GET". Need määravad ära, et nupu (<input type="submit">) vajutamisel minnakse aadressile "." (mis on praegune lehekülg) ja lisatakse parameetrid vastavalt sisendkastidele ja nende "name" atribuutidele.
Aadressiparameetreid oskame juba lugeda. Nüüd on jäänud ainult küsimus: kuidas me sellisel leheküljel andmeid kuvame?
Kui lehekülg oli ainult üks sõne, siis oli sinna andmeid lihtne sisestada:
return "Sinu juhuslik arv on {}.".format(juhuslik_arv) |
Kui kasutame HTML-koodi ja render_template() funktsiooni, siis on lahendus natuke keerulisem, aga siiski sarnane. HTML-koodis tuleb lisada muutujad kahe loogelise sulu vahele:
<h2>Sinu juhuslik arv on: {{arv}}</h2> |
Need väärtused tuleb lisada render_template() funktsiooni parameetritena:
juhuslik_arv = random.randint(1, 10) |
See töötab, sest Flask kasutab Jinja malle. Jinjaga saab teha palju enamat, kui ainult andmeid kuvada. Näiteks saab selle abil lisada HTML-koodi if-lauseid (kas muutuja saadeti üldse kaasa? kas on vaja kuvada veateade? kas kasutaja on sisselogitud?) või for-tsükleid (kuva kõik järjendi elemendid). Siin me neid täpsemalt läbi ei katseta.
Proovi panna kokku kõik siiamaani tehtud kood ja saada juhusliku arvu generaator lõpuks valmis.
Kui Flaski server jookseb, saab samas arvutis sellele ligi aadressil 127.0.0.1:5000, aga teised sellele ligi ei saa. Idee poolest saab ligi samas võrgus teisest arvutist või isegi teisest võrgust, kui ruuter seda lubab, aga eesmärk on saada lihtne ligipääs kõigile. Selle jaoks tuleb panna rakendus püsti kuskile serverisse. Seda lubavad lihtsasti teenused nagu PythonAnywhere ja Heroku. Saab ka kasutada virtuaalservereid nagu DigitalOcean (õpetus), mis on natuke keerulisem.
PythonAnywhere lubab Flaski veebirakendust tasuta üles laadida mõningate piirangutega. Veebirakendus ilmub aadressile
https://kasutajanimi.eu.pythonanywhere.com/.
Juhised:
https://www.pythonanywhere.com/registration/register/beginner/
Kui rakendus avalikuks teha, peaks koodist ära võtma debug=True.
Ehitasime väga lihtsa veebirakenduse ja panime selle Internetti üles. Veebirakendused võivad muidugi olla palju keerulisemad ja järgmise YouTube'i, Instagrami või ÕIS-i versiooni loomiseks on veel palju õppida. Developer Roadmap teeb hea ülevaate sellest, mida kõike peab korralik veebiarendaja teadma.
Flaskiga jätkamiseks tasub järgida selle dokumentatsiooni õpetust, kus kirjutatakse võimas blogirakendus. Kui Flask ei meeldi, siis Django raamistik teeb rohkem asju sinu eest ära, aga sellega alustamine võib olla keerulisem.
Informaatika õppekavas minnakse süvitsi veebirakendustesse aines "Veebirakenduste loomine" (LTAT.05.004).
1. Juhusliku arvu generaatoril lähevad alam- ja ülempiirid nupu vajutamisel tagasi vaikeväärtustele. Täienda rakendust nii, et need säilitavad oma väärtused vastavalt parameetritele.
2. Täienda juhusliku arvu generaatorit nii, et genereerida saab mitu juhuslikku arvu. Selleks tuleb lisada uus aadressiparameeter, mis määrab ära, mitu arvu peab genereerima. Tuleb ka lisada HTML-koodi sisendikast, mis lubab seda parameetrit muuta. Näide:
<label for="kogus">Kogus:</label> |
3. Kirjuta uus kasulik veebirakendus, mis kasutab kasutajasisendit. Mõned ideed:
Soovitatav on taaskasutada juhusliku arvu generaatori HTML- ja CSS-koodi.
Siiamaani oleme salvestanud andmeid erinevatesse andmestruktuuridesse: järjendid, ennikud, hulgad, sõnastikud. Vaatamata on veel jäänud vaieldamata kõige võimsam andmestruktuur üldse. See on andmestruktuur, millega luuakse teisi andmestruktuure. See on andmestruktuur, mida ei saa võibolla isegi kutsuda andmestruktuuriks, sest see on omaette klass.
Jutt käib justnimelt klassidest ja nendega loodavatest objektidest. Tegu on objektorienteeritud programmeerimise peatükiga. Vaatame, kuidas oleme juba varem klassidega kokku puutunud, kuidas luua enda klasse, mis nendega teha saab ning kus neid hästi rakendada saab. Proovime luua enda versiooni mõnest populaarsest rakendusest.
Selle peatüki läbimiseks piisab Pythoni installatsioonist: midagi paigaldama ei pea.
Et peatükist aru saada, peab olema läbitud õpiku esimene osa.
Kuigi siin kursusel pole veel klasside telgitagustesse vaadatud, oleme mõningaid klasse kasutanud. Tuletame meelde datetime moodulit. See on moodul, mis lubab salvestada ajahetki, sh kuupäevi ja kellaaegu.
Mooduli importimine:
>>> import datetime |
Moodulis datetime asub klass nimega datetime. Klassidega saab teha isendeid ehk objekte. Need on muutujad, mille küljes võib olla teisi muutujaid ehk atribuute. Need muutujad võivad olla ka funktsioonid, mida kutsutakse meetoditeks. Et luua uut objekti, peab klassi välja kutsuma kui funktsiooni, mis tagastab selle klassi isendi. Proovime luua datetime isendi. Funktsiooni parameetriteks lähevad aastaaeg, kuu, päev, tund, minut, sekund ja mikrosekund.
>>> kuupäev = datetime.datetime(2020, 2, 29, 12, 34, 56, 789) |
Muutujasse kuupäev on nüüd salvestatud klassi datetime isend ehk objekt. Uurime, mis on selle muutujatüüp (type()) ning mis muutujad sellega seostuvad (dir()).
>>> type(kuupäev) |
Näeme, et tegemist on tõepoolest isendiga klassist datetime.datetime, millel on palju seotud muutujaid ehk välju. Näiteks on seal väljad year, month, day, hour, minute, second ja microsecond, millega saame kätte arvud, millega me isendi lõime. Neid välju nimetatakse isendiväljadeks.
>>> kuupäev.year |
Klassidel saab tihti isendiväljade väärtusi ise määrata (nt kuupäev.year = 2021), aga datetime seda ei luba.
Loodud isendil on ka funktsioone, mis kasutavad mõnda isendivälja. Neid nimetatakse isendimeetoditeks. Näiteks on üks selline strftime(), mis tagastab kuupäeva sõnena soovitud formaadis.
>>> kuupäev.strftime |
Klassidel on tihti isendimeetodeid, mis muudavad isendivälju, aga datetime klassil selliseid pole.
Klassidel on ka funktsioone, mis ei vaja isendeid ega isendivälju. Nendega saab üldiselt teha midagi selle klassiga seotut, näiteks sama klassi isendi tagastamist. Selliseid funktsioone kutsutakse staatilisteks meetoditeks. Sisuliselt on need lihtsalt funktsioonid, mis on klassi küljes. Klassil datetime on näiteks staatiline meetod now(), mis tagastab praeguse ajahetke jaoks datetime isendi.
>>> datetime.datetime.now() |
Selliseid meetodeid saab ka välja kutsuda isendite kaudu, aga üldiselt nii ei tehta.
>>> kuupäev.now() >>> datetime.datetime.now().now().now().now() |
Klassidel võib ka olla staatilisi välju, mis on kõikidel isenditel samad. Klassil datetime on näiteks staatilised väljad min ja max, mis näitavad minimaalseid ja maksimaalseid kuupäeva väärtuseid.
>>> datetime.datetime.min |
Väljade ja meetodite selgituste saamiseks saab kasutada dokumentatsiooni ja help() funktsiooni.
>>> help(datetime.datetime) | ... |
Järgmisena vaatame, kuidas klasse luua ning kuidas need töötavad.
Loome enda kuupäeva klassi. Et vähendada tööd, mõtleme ainult kuupäevadele ilma kellaaegadeta ehk meie kuupäeva klassil peaks olema 3 isendivälja: aasta, kuu ja päev. Meie ideaalses maailmas on kalendris igal kuul täpselt 31 päeva, muidu peaks kontrollima kuu päevade arvu vastavalt kuule.
Klassidele on tavaks panna suure algustähega nimi, seega paneme selle nimeks Kuupäev. Klasse saab luua class käsuga. Selle käsu alla saab defineerida meetodeid ja välju. Lisame alguses ainult aasta välja ning määrame selle väärtuseks 2020.
class Kuupäev: |
Nüüd saame luua selle klassi isendi. Selleks kutsume seda välja kui funktsiooni. Täpselt nagu tavaliste muutujatega, saame isendivälju kasutada ning üle kirjutada. Saame ka uusi välju omistada ning neid kasutada.
>>> kuupäev = Kuupäev() |
Praegu on igal uuel loodud isendil aasta väärtus automaatselt 2020. See ei ole väga ettenägelik lähenemine, sest sellest järgnevatel aastatel ei ole see enam kasulik ja iga uue isendi loomisel peab selle üle kirjutama. Samuti on tüütu isendi loomisel manuaalselt teisi muutujaid määrata.
Kui me lõime uue datetime isendi, kutsusime me klassi välja nagu funktsiooni ja lisasime muutujate väärtused parameetritena.
>>> kuupäev = datetime.datetime(2020, 2, 29, 12, 34, 56, 789) |
Et sellist võimalust enda klassile lisada, peame defineerima erilise funktsiooni, mida kutsutakse konstruktoriks. Selle funktsiooni nimi Pythonis on __init__ ning selle esimene parameeter peab olema self, mille väärtuseks läheb alati käesolev isend. Järgnevad parameetrid võivad olla ise määratud. Need on need parameetrid, mis lähevad funktsiooni sulgudesse selle väljakutsumisel.
class Kuupäev: |
Defineerisime klassile Kuupäev konstruktori kolme parameetriga: aasta, kuu ja päev. Kuna self tähendab käesolevat isendit, siis määrame uuele isendile uued isendiväljad, mille väärtused võtame funktsiooni parameetritelt samamoodi, nagu me enne omistasime isendile välju.
Nüüd saame luua uue isendi ise valitud algväärtustega.
>>> kuupäev = Kuupäev() |
Konstruktoris saame ka kontrollida väärtuseid. Kui kuu või päeva väärtus on sobivatest väärtustest väljaspool, viskame erindi. Meie klassil on kõikidel kuudel on teadagi 31 päeva.
class Kuupäev: |
>>> kuupäev = Kuupäev(2020, 13, 1) |
Kui me tahame praegu kuupäeva isendit teisendada sõneks, siis peame kokku liitma 3 isendivälja.
>>> kuupäev = Kuupäev(2020, 2, 29) |
Võime lisada klassile isendimeetodi, mis seda ise teeb ning tagastab kuupäeva sõnevormingus. Asendame lihtsalt isendi viite muutujaga self.
def sõnena(self): |
>>> kuupäev = Kuupäev(2020, 2, 29) |
Tegelikult peaks igal klassil olema selline meetod, mis väljendab isendit sõnena. Kui proovime isendit praegu sõneks teisendada, siis antakse meile klassi nimi ja mäluviide, aga isendi sisu ei näidata.
>>> str(kuupäev) |
Kui aga datetime klassi isendit sõneks teisendada, siis tuleb ülevaade isendiväljadest.
>>> kuupäev = datetime.datetime(2020, 2, 29, 12, 34, 56, 789) |
See, mida sõneks teisendamisel tehakse, oleneb jälle ühest erilisest meetodist nimega __str__. Kui sõneks teisendatakse, võetakse selle meetodi tagastus. Kirjutame oma kuupäeva klassile selle meetodi. Võime kasutada sõnena meetodit selle tagastuses.
def __str__(self): |
>>> kuupäev = Kuupäev(2020, 2, 29) |
Sõneks teisendamine töötab hästi, aga niisama isendi kirjutamine tagastab jälle klassi nime ja mäluviite. See on sellepärast, et see väärtus võetakse teisest meetodist nimega __repr__ (tuleb sõnast representation). Saame enda klassile selle meetodi kirjutada. Tavaks on sellel meetodil tagastada sõne, mille abil saab konstruktorit välja kutsuda, et saada selline isend.
def __repr__(self): |
>>> kuupäev = Kuupäev(2020, 2, 29) |
Lisame veel isendimeetodi, mis muudab isendiväljade väärtuseid. Näiteks võiks olla meetod aastate juurde lisamiseks.
def lisa_aastaid(self, aastate_arv): |
>>> kuupäev = Kuupäev(2020, 2, 29) |
Kuude lisamine on keerulisem, sest kui kuu väärtus on üle 12, siis peab vastavalt aastaid juurde lisama.
def lisa_kuid(self, kuude_arv): |
>>> kuupäev = Kuupäev(2020, 2, 29) |
Proovi ise lisada päevade lisamise meetod. Kui kasutada lisa_kuid meetodit, siis ei pea aastaid eraldi juurde lisama. Lihtsuse mõttes võib ikka eeldada, et igal kuul on 31 päeva.
Kui kõik kolm meetodit on tehtud, saab ka teha ühise liitmismeetodi.
def lisa(self, aastate_arv=0, kuude_arv=0, päevade_arv=0): |
>>> kuupäev = Kuupäev(2020, 2, 29) |
Lisame oma klassile ühe staatilise meetodi. Staatiliste meetodite puhul peab lihtsalt parameetri self ära jätma, sest ei kasutata ühtegi isendivälja ega -meetodit. Kirjutame meetodi, mis tagastab isendi praeguse kuupäeva väärtustega. Võtame väärtused datetime isendist.
def praegu(): |
>>> Kuupäev.praegu() |
Proovi kirjutada staatiline meetod parsi, mis võtab parameetrina sõne formaadis "YYYY-MM-DD", nt "2020-02-29" ning tagastab uue kuupäeva isendi nende väärtustega.
>>> Kuupäev.parsi("2020-02-29") |
Vaatasime natuke Pythoni objektorienteeritud programmeerimise võimalusi: mis asjad on klassid ja isendid, kuidas neid luua ja kasutada ning mis on, isendiväljad ja meetodid. Mõned võimalused jäid vaatamata, näiteks privaatsed väljad, klasside pärilus, itereeritavad isendid, objektide käitumine liitmisel, lahutamisel, võrdlemisel jne. Nendega saab tutvuda Pythoni dokumentatsioonis.
Programmeerimise paradigmat, mis kasutab objekte, nimetatakse objektorienteeritud programmeerimiseks. See on väga levinud viis, kuidas programme kirjutada ning sellest räägitakse rohkem aines "Objektorienteeritud programmeerimine" (LTAT.03.003), kus kasutatakse Java keelt. Kui on plaanis edaspidi Pythoniga tegeleda, siis on Pythoni objektorienteerituse tundmine väga kasulik.
1. Lõpeta kuupäeva klass. Peavad olema realiseeritud meetodid lisa_päevi ja parsi. Meetodid peavad arvestama päevade arvuga kuudes: jaanuaris on 31, veebruaris on 28-29, märtsis on 31, aprillis on 30 jne. Veebruari päevade arv sõltub aastast: kui aasta jagub arvuga 4, siis on 29, välja arvatud siis, kui see jagub arvuga 100, siis on 28, välja arvatud siis, kui see jagub arvuga 400, siis on jälle 29.Aastal 1900 oli veebruaris 28 päeva, aastal 2000 oli veebruaris 29 päeva.
2. Mõtle välja klass, mis võib sulle kasuks tulla ning kirjuta programm selle klassi defineerimise ja kasutusnäidetega. Klassil peab olema konstruktor, vähemalt üks isendiväli, vähemalt üks isendimeetod ning meetodid __str__ ja __repr__.
Mõned ideed:
Mängude loomine on väga suur valdkond, kus programmeerijad saavad oma võimeid kasutada. Mänge on muidugi võimalik ka Pythoniga luua. Selle jaoks on loodud erinevaid teeke: Pygame, pyglet, Cocos2d, Panda3D ja palju muid. Siin peatükis vaatame, kuidas luua üks lihtne mäng Pygame'iga.
Pygame on populaarne Pythoni mängude loomise teek, mis lubab lihtsasti tekitada graafikat ja heli ning saada kasutajalt sisendit mitmel erineval moel. See on hea viis, kuidas alustada oma mänguarendaja karjääri. See pole ka ainult alustamiseks: päris palju mänge on Pygame'iga tehtud.
Enne jätkamist tuleb paigaldada moodul pygame. Seda saab teha käsuga pip install pygame. Katsetamiseks kirjuta Pythonis import pygame. Kui erindit ei visata, on moodul paigaldatud. Kui ei ole kindel, kuidas mooduleid installida, siis on õpikus moodulite paigaldamise juhised.
Et peatükist aru saada, peab olema läbitud õpiku esimene osa. Soovitatav on läbida peatükk "Objektorienteeritud programmeerimine" läbimine, aga see ei ole vajalik.
Kui moodul pygame importida, siis saab Pygame'i käivitada käsuga pygame.init(). Enne seda peab veel määrama akna suuruse käsuga pygame.display.set_mode(), mille parameetriks on ennik ekraani laiusega ja kõrgusega pikslites. Pygame'i akent saab sulgeda käsuga pygame.quit(). Teeme akna laiusega 640 pikslit ja kõrgusega 480 pikslit.
import pygame |
Kui see programm käivitada, siis ilmub korraks must aken suurusega 640 korda 480 pikslit ja läheb kinni.
Pygame'i loodud aken töötab eraldiseisvalt Pythoni programmile. See tähendab, et pärast pygame.init() käsku kuvatakse eraldi aken, kuhu saab hakata joonistama ning programmi kood jookseb edasi. Aken läheb kinni alles siis, kui programm jõuab sulgemiskäsuni pygame.quit(). Kui enne sulgemiskäsku jookseks programm 3 sekundit, siis aken on püsti 3 sekundit.
Kui enne sulgemiskäsku jooksutada lõpmatu tsükkel, siis aken ei lähegi kinni ning ristist sulgemine ka ei aita. Programmi töö peab muudmoodi lõpetama. Saame selles lõputus while-tsüklis kontrollida, kas kasutaja on vajutanud sulgemisnupule ja sel juhul saame tsükli lõpetada.
import pygame |
Kui see programm käivitada ja ristile vajutada, siis aken läheb kinni. Oleme kirjutanud väga algelise programmi, millega kasutaja saab suhelda! Vaatame hiljem, kuidas tsüklisse muud funktsionaalsust lisada ja teistmoodi kasutajalt sisendit saada. Kõigepealt proovime aknasse midagi joonistada.
Siiamaani oleme loonud musta akna, mis on graafilisest mängust tsipa kaugel. Järgmisena proovime selle akna millegi värvilisega täita.
Saame täita selle akna mingi värviga, mida saab esindada erinevatel viisidel, aga Pygame'is on tavaks kasutada RGB mudelit, mis koosneb punasest, rohelisest ja sinisest värvi kogusest, 0 kuni 255. Neid esindatakse kolmeliikmeliste ennikutena. Näiteks on must värv (0, 0, 0), valge värv (255, 255, 255), punane värv (255, 0, 0), akvamariinsinine (128, 255, 212) jne. Lihtne viis enda värvi valimiseks on guugeldada "colour picker" ja võtta valitud värvi RGB väärtused.
Täidame akna enda soovitud värviga. Selleks peame ekraanisuuruse muutmise funktsiooni tagastuse salvestama muutujasse ekraan. See on muutuja, kuhu hakkame edaspidi kõiki joonistusi lisama. Ekraani saab täita meetodiga fill(), mille parameetriks läheb värv. Tehtud muudatused tuleb salvestada käsuga pygame.display.flip().
import pygame |
Programm käivitab nüüd akna, mis pole enam must, vaid akvamariinsinine. Edusammud!
Proovime lõpuks aknasse midagi lisada. Pygame võimaldab joonistada erinevaid kujundeid. Ristkülikut saab ekraanile joonistada funktsiooniga pygame.draw.rect(). See nõuab kolme parameetrit:
Koordinaatide lugemist alustatakse Pygame'is ülevalt vasakult ehk (0, 0) on üleval vasakul, (640, 0) on üleval paremal, (0, 480) on all vasakul ja (640, 480) on all paremal. Kõik vahepealsed on kuskil ekraani sees.
Joonistame roosa ristküliku, mis algab meie akna keskpunktist ehk (320, 240) ning on 200 piksli laiune ja 100 piksli kõrgune.
import pygame |
Pygame lubab ka teisi kujundeid joonistada. Loe dokumentatsioonist erinevaid võimalusi ja proovi ekraanile joonistada ring, kolmnurk ja joon.
Sarnaselt kujunditele saab mängu lisada pilte. Kõigepealt peame leidma pildi, mida mängus kasutada. Neid saab muidugi ise luua, aga Internetis on suur hulk vabalt kasutatavaid materjale olemas: itch.io/game-assets, opengameart.org jpm. Kui neid kasutada mängudes, on vaja autorile viidata. Siin peatükis kasutame rotti, mis on võetud siit, ja juustu, mis on võetud siit.
Et joonistada pilt aknasse, tuleb see kõigepealt sisse laadida pildiobjektina funktsiooniga pygame.image.load(). Sulgudesse läheb failitee. Praegu paigutame pildifailid programmifailiga samasse kausta, aga suurema mängu puhul võiks need olla eraldi kaustas.
rott = pygame.image.load("rott.png") |
Muutuja rott on nüüd pind (Surface), mida saab joonistada ekraanile. Pindade võimaluste kohta saab lugeda Pygame'i dokumentatsioonist.
Saame roti paigutada ekraanile meetodiga blit(). Sulgudesse läheb pildifail ning koordinaadipaariga asukoht. Paigutame loodud roti objekti ekraani koordinaatidele (100, 200).
ekraan.blit(rott, (100, 200)) |
Roti laiuse ja kõrguse eraldi saame kätte meetoditega get_width() ja get_height(). Proovi paigutada rott täpselt ekraani keskele, kasutades seda meetodit, et arvutada paigutuse koordinaadid. (320, 240) ei ole õige siin vastus.
>>> rott.get_width() |
Mängudes on tavaliselt teksti, mis näitab vestluseid, õpetusi, skoori, menüüd jpm. Pygame võimaldab teksti kuvada samamoodi nagu piltegi.
Kõigepealt tuleb valida font, milles teksti kuvada ja teha sellest objekt funktsiooniga pygame.font.SysFont(). Esimeseks parameetriks läheb fondi nimi sõnena ja teiseks läheb fondi suurus. Kui fondi nimeks valida None, siis valib Pygame vaikimisi fondi. Funktsiooniga Font() saab ka fondi võtta failist. See on kasulik, kui fonti ei pruugi olla teiste arvutis.
font = pygame.font.SysFont("Comic Sans MS", 24) |
Kasutades fonti saame meetodiga render() luua pinna objekti. Sulgudesse läheb tekst sõnena, tõeväärtus True ning teksti värv. Tõeväärtuse kohta võib lugeda dokumentatsioonist. Loodud objekt on põhimõtteliselt pilt ehk seda saab meetodiga blit() paigutada ekraanile. Loome teksti "Tere, maailm!" musta fondiga ning paigutame selle koordinaatidele (40, 40).
tekst = font.render("Tere, maailm!", True, (0, 0, 0)) |
Kogu kood siiamaani:
import pygame |
Pinna objekte saab moondada: pööramine, peegeldamine, suurendamine jne. Uuri dokumentatsiooni: pygame.transform.
Harjuta ekraanile joonistamist. Leia vastused küsimustele:
Siiani oleme joonistanud ekraanile ühe kaadri. Et saada paigutatud objekte liikuma, ei saa me lihtsalt muuta nende asukohti: me peame iga kaadri algusest peale joonistama. See võib kõlada ebaintuitiivsena, aga Pygame töötabki just niimoodi.
Jätame alles roti ekraanile, aga liigutame tema joonistamise meie põhitsüklisse, mis programmi käimas hoiab.
import pygame |
Kui programm käivitada, siis midagi väga muutunud ei ole. Rott on ikka paigal, ta lihtsalt joonistatakse igal tsükli käigul uuesti. See on kasulik, sest nüüd me saame näiteks muuta igal käigul roti asukohta.
Salvestame programmi alguses roti koordinaadid mingitesse muutujatesse, näiteks rott_x ja rott_y, algul olgu need 100 ja 200. Nüüd igal tsükli korral suurendame rott_x muutujat ühe võrra ja käivitame programmi uuesti.
rott_x = 100 ... |
Kui see programm käivitada, siis rott tõepoolest liigub väga kiiresti paremale ja jääbki liikuma. Proovi täitmiskäsk ekraan.fill() tsüklist ära võtta ja ainult programmi algusesse panna. Mis juhtub?
Saime roti liikuma, aga mitte väga hästi. Probleem on praegu selles, et tsükli käik ei ole kuidagi ajaliselt piiratud: see kestab täpselt nii kaua, kui Pythonil läheb selle jooksutamiseks. See on esiteks väga lühike aeg: peaksime roti koordinaati liigutama palju vähem (nt 0.1 pikslit) iga kord. Teiseks: see võib erineda erinevates arvutites ja isegi samas arvutis erinevatel aegadel. Tahame, et mäng jookseks kõikides arvutites samamoodi igal hetkel.
Selle probleemi parandamiseks kasutame Pygame'i kella. Loome programmi alguses uue kella objekti:
kell = pygame.time.Clock() |
Iga tsükli alguses peaks kell tiksuma ehk kutsume välja meetodi tick(). Sulgudesse läheb arv, mitu kaadrit peaks sekundis kuvama. Valime 60, sest enamik kuvaritest värskenduvad 60 korda sekundis. Selle meetodi käivitamisel ootab programm umbes 1/60 sekundit.
dt = kell.tick(60) |
See meetod tagastab, mitu millisekundit on kulunud eelmisest kella tiksumisest. 60 kaadri sekundis puhul see on enamasti 16 või 17. Seda on väga kasulik teada, sest kui arvuti näiteks kiilub korraks kinni ja mõnda kaadrit joonistatakse natuke kauem, siis meile on see teada ja me saame seda oma mängus arvestada nii, et mängu tegevus ei kiiluks kinni, vaid liiguks vastavalt sellele, kui palju aega on kulunud. Kui kaader peaks kestma 16 millisekundit ja kestab hoopis 32, siis mängu tegevus peaks sellel kaadril liikuma 2 korda kiiremini. Selle abil tagame ka selle, et mäng jookseb kõikidel arvutitel sama kiiresti.
Seega peame igal liikumisel arvestama kulunud aega (muutujat dt). Roti liikumisel korrutame sellega läbi.
rott_x += 0.25 * dt |
0.25 on siin roti kiirus, mis väljendab mitu pikslit me tahame, et rott liiguks millisekundis. 0.25 pikslit millisekundis on teisendatult 250 pikslit sekundis. Mitme sekundiga jõuab rott ekraanist välja, kui ekraani laius on 640 ja roti algne x-koordinaat on 100?
Kogu kood:
import pygame |
Proovi muuta kaadrisagedust näiteks 24 peale, mida kasutatakse tavaliselt filmides. Kas rott jõuab erinevate kaadrisagedustega ekraanist välja joosta täpselt samade ajakuludega? Kui jah, siis programm töötab igas arvutis sama kiiresti, lihtsalt aeglase arvuti puhul võivad mõned kaadrid vahele jääda.
Proovime veel animatsiooni huvitavamaks teha. Kui rott jõuab teisele poole, siis paneme ta ümber pöörama. Selleks salvestame roti kiiruse eraldi muutujasse.
roti_kiirus = 0.25 ... ... |
Kui roti parem ots jõuab paremalt poolt ekraanist välja, siis lihtsalt teeme tema kiiruse negatiivseks. Teeme sama, kui roti vasak ots jõuab vasakult poolt ekraanist välja.
if (rott_x + rott.get_width()) > 640 or rott_x < 0: |
Kui roti kiirus on negatiivne, siis tema x-koordinaat väheneb igas tsükli käigus ja ta liigub vasakule.
Pöörame ka roti ümber. Rohkem infot dokumentatsioonis.
rott = pygame.transform.flip(rott, True, False) |
Kogu kood:
import pygame |
Rott nüüd põrkab lõputult akna parema ja vasaku ääre vahel. Proovi panna rott ringi liikuma, nagu vana DVD-mängija ekraanisäästja: kui jõuab ääreni, siis põrkab. Vihje: liikuval kahemõõtmelisel esemel saab olla 2 kiirust: horisontaalkiirus ja vertikaalkiirus.
Panime roti liikuma, aga tavaliselt saab mänge mõjutada kasutaja. Paneme roti liikuma vastavalt klahvivajutustele!
Siiani oleme tegelikult mingit kasutajasisendit küsinud: kui ristile vajutada, siis mäng läheb kinni. Senist sisendipüüdmist peame natuke muutma. Kahe tsükli käigu vahel võib toimuda mitu sisendit, aga me oleme iga kord ainult ühe küsinud - peaksime kõike kontrollima. Kõik sisendid saab kätte funktsiooniga pygame.event.get(). Kuna need peab tsükliga läbi käima, siis break viiks meid ainult sellest tsüklist välja. Asendame selle sys.exit() väljakutsega, mis lõpetab programmi töö.
for sisend in pygame.event.get(): |
Samamoodi, nagu kontrollime ristile vajutamist, saame kontrollida ka klaviatuurivajutusi. Selle jaoks tuleb kontrollida, kas sisendi tüüp on pygame.KEYDOWN. Kui on, siis saame vaadata selle klahvi väärtust. Nooleklahvid on näiteks pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT. Vastavalt nendele võiksime rotti liigutada. Erinevaid nuppe saab kätte dokumentatsioonist.
for sisend in pygame.event.get(): |
Kui nupule vajutada, siis rott tõepoolest liigub… natuke. Kui tahame, et nuppu all hoides rott jääkski liikuma, siis võiksime koordinaatide muutmise asemel hoopis roti kiiruseid muuta ning igal tsükli käigul rotti liigutada vastavalt kiirustele.
roti_kiirus = 0.25 ... if sisend.key == pygame.K_UP: ...
|
Kui nüüd nupule vajutada, siis rott jääbki liikuma. See võib mõnes mängus olla soovitud, näiteks ussimängus, aga vaatame, kuidas nupust lahti laskmisel rott seisma jääks. Saame vaadata, et kui sisenditüüp on pygame.KEYUP ehk klahvist lastakse lahti, siis nullime kiirused vastavalt klahvile, mis lahti lasti. Kogu kood siiani:
for sisend in pygame.event.get(): |
Selle programmiga on veel probleeme. Kui hoida all paremat noolt, seejärel hoida all vasakut noolt ja lasta parem nool lahti, siis rott jääb seisma, kuigi vasak nool on veel all. Saame igal hetkel kontrollida vajutatud klahve funktsiooniga pygame.key.get_pressed(). See on sõnastik, mille võtmeteks on klahvid ja väärtusteks on tõeväärtused, kas nuppu hoitakse all või mitte.
import pygame |
Mõningaid probleeme veel leidub. Kui hoida all nii üles- kui allanoole klahv, siis liigub rott alati ülespoole, sest seda kontrollitakse esimesena. Roti kiirus peaks igas suunas olema võrdne, aga diagonaalis liikudes on ta kiirem. Praeguseks on aga liikumine piisavalt hea, liigume edasi.
Mis mäng on mäng ilma eesmärgita? Lisame mängu juustu, mille rott peab kätte saama.
Laadi alla juust.png ülevalt ja tuleta meelde, kuidas laadida see mängu ning joonistada see. Juustul võiks olla ka koordinaadid muutujatena, sarnaselt rotile.
Kui proovida juustu peale liikuda, siis midagi ei juhtu. Paneme kokkupuutumisel juustu juhuslikku asukohta hüppama!
Kuidas me kontrollime kokkupuudet? Saaksime manuaalselt vaadata, kui roti koordinaadid on juustu koordinaatide lähedal, aga see on palju tööd ning Pygame lubab seda palju lihtsamalt teha.
Kui me paigutame pinna ekraanile meetodiga blit(), siis see tegelikult tagastab meile ühe ristküliku objekti (Rect). See on kasulik objekt, millelt saab kätte pildi koordinaate: keskpunkt, nurgad, servade keskpunktid, laius, kõrgus jne. Muuseas saab sellega kontrollida kahe ristküliku kokkupuutumist üksteisega, kasutades colliderect() meetodit. Liigutame kokkupuutumisel juustu uuele juhuslikule asukohale.
juust_rect = ekraan.blit(juust, (juust_x, juust_y)) |
Lõplik kood kommentaaridega:
import pygame |
Kui Pythonis klasside kasutamine on selge, siis võiks igale objektile teha enda klassid, millel on isendiväljad suurustest ja kiirustest ning meetodid liigutamise ja muude tegevuste jaoks: https://stackoverflow.com/a/35642955/12123296
Vaatasime, kuidas Pygame töötab, joonistasime kujundeid, pilte ja teksti, panime pildi liikuma, liigutasime ise pilti klahvivajutustega, kontrollisime kahe eseme kokkupuudet ja saime lõpuks valmis midagi, mida saab enam-vähem mänguks nimetada. Graafiliste mängude loomise alustalad on nüüd all, aga väga palju jäi käsitlemata.
Et edasi õppida, pakub Pygame erinevaid õpetusi oma veebilehel. Tasub ka uurida Pygame'i projektide kogumikku. Sinna on postitatud palju erinevaid Pygame'iga tehtud mänge koos lähtekoodidega (Releases all), mida tasub uurida.
Ülikoolis saab arvutigraafikaga ja mängudega jätkata ainetes "Arvutigraafika" (MTAT.03.015) ning "Arvutimängude loomine ja disain" (MTAT.03.263).
1. Täienda peatüki jooksul loodud mängu skooriga, mis loeb juustu kättesaamise arvu. Kuva see skoor kuskil mänguaknas tekstina.
2. Kirjuta Pygame'iga animatsioon, kus mingi pilt või kujund liigub sinusoidi trajektoori või ringjoone järgi.
3. Kirjuta ise Pygame'iga üks mäng, millel on mingi eesmärk. Graafika joonistada ise või võtta Internetist. Viimase puhul tuleb lähtekoodis kommentaarina allikale viidata. Mõned ideed:
Käesolev Pythoni kursus teeb hea sissejuhatuse Pythoniga programmeerimisse, aga puhtalt aine läbimisega ennast võluriks kutsuda ei saa. Selle jaoks peab läbima selle viimase silmaringimaterjalide peatüki.
Siin peatükis vaatame, kuidas teha lihtsaid asju keerulisemalt, et vähendada üleliigse koodi kirjutamist. Õpime keerulisemaid Pythoni võimalusi, et teha elu lihtsamaks.
Selle peatüki läbimiseks piisab Pythoni installatsioonist: midagi paigaldama ei pea.
Et peatükist täiesti aru saada, peab olema läbitud õpiku esimene ja teine osa.
Vahepeal on vaja millegi väga lihtsa jaoks kasutada if-lauset. Näiteks, sõna kääne muutub osastavaks, kui see viitab mitmele esemele: ostukorvis on 1 toode, aga ostukorvis võib olla mitu toodet.
Sellises olukorras peame kasutama if-lauset:
if toodete_arv == 1: |
Et vähendada kirjutatud koodi, saab if-lause kirjutada ühele reale:
print("Ostukorvis on {} {}".format(toodete_arv, "toode" if toodete_arv == 1 else "toodet")) |
Lühema if-lause valem:
väärtus_kui_tõene if tingimus else väärtus_kui_väär |
Näited:
>>> "tõene" if True else "väär" |
Proovi kirjutada lühike tingimuslause, mis annab väärtuseks "fizz", kui arv jagub kolmega ning arvu ise, kui see ei jagu kolmega.
Meil on järjend, mis sisaldab esemeid, mida on keelatud lennukile kaasa võtta keelatud.
keelatud = ["veepudel", "kahvel", "žilett", "ilutulestikud", "elevant"] |
Kirjuta programm, mis küsib kasutajalt eset ning väljastab, kas see on keelatud või mitte nii, et programmil ei ole ühtegi taanet.
Sisesta ese: telefon telefon ei ole keelatud lennukile kaasa võtmiseks |
Sisesta ese: veepudel veepudel on keelatud lennukile kaasa võtmiseks |
Kui tahame luua uut järjendit ja täita seda mingite andmetega, oleme siiamaani loonud uue järjendi ning seejärel for-tsükliga sinna elemente juurde lisanud.
Näiteks, meil on järjend sõnadest ja me tahame saada järjendit nende pikkustest.
sõned = ["Python", "on", "üldotstarbeline", "interpreteeritav", "programmeerimiskeel"]
|
Sama asja saab kirja panna vähema koodiga:
sõned = ["Python", "on", "üldotstarbeline", "interpreteeritav", "programmeerimiskeel"]
|
Seda võtet nimetatakse järjendi hõlmamiseks (ingl. k. list comprehension).
Veel üks näide: tahame saada kahe astmeid. Pikemalt saab teha nii:
kahe_astmed = [] |
Järjendi hõlmamisega:
kahe_astmed = [2**arv for arv in range(10)] |
Ülesanne võib olla natuke keerulisem. Näiteks soovime kahe astmeid ainult siis, kui need lõppevad neljaga.
kahe_astmed_neljaga = [] |
Ka lisatingimusi saame panna järjendi hõlmamisse:
kahe_astmed_neljaga = [2**arv for arv in range(23) if 2**arv % 10 == 4] |
Proovi järjendi hõlmamisega luua järjend, mis sisaldab ainult arve, mis ei jagu arvudega 3 ega 5:
[1, 2, 4, 7, 8, 11, 13, 14, 16, 17, 19, ...] |
Meil on järjend, mis sisaldab kõige sagedasemaid sõnade põhivorme (lemmasid) koos nende järkudega:
[(1, "olema"), (2, "ja"), (3, "see"), (4, "tema"), (5, "mina"), (6, "ei"), (7, "et"), (8, "kui"), (9, "mis"), (10, "ka")] |
Proovi järjendi hõlmamisega luua järjend, mis jätab alles ainult sõnad:
["olema", "ja", "see", "tema", "mina", "ei", "et", "kui", "mis", "ka"] |
Proovi hõlmamist ka kuhjadega, sõnastikega ja ennikutega.
Kui me tahame sorteerida arvude järjendit, siis saab kasutada funktsiooni sorted või meetodit sort.
>>> arvud = [1, 5, 9, 2, 6, 5] |
Mis siis, kui meil on vaja midagi keerulisemat sorteerida? Näiteks meil on järjendisse salvestatud ennikud, mille üks element on arv. Kuidas öelda sorteerimisfunktsioonile, et sorteerida arvu järgi?
maletajad = [("Ding Liren", 3), ("Nepomnjaštši", 4), ("Caruana", 2), ("Carlsen", 1)] |
Sorteerimisfunktsioonidel on parameeter key, millele saab panna väärtuseks funktsioone. See funktsioon jooksutatakse iga järjendi elemendi peal läbi nii, et parameetriks pannakse element ning järjend sorteeritakse selle funktsiooni tagastuste põhjal. Meie järjendi puhul saaksime teha funktsiooni, mis tagastab enniku teise liikme.
def teine_liige(ennik): |
Nüüd peab meelde tuletama, et funktsioonid on ka muutujad, lihtsalt neid saab välja kutsuda. Seega meie funktsioon on salvestatud muutujasse teine_liige. Sorteerime järjendi nii, et key parameetri väärtuseks läheb see funktsioon.
>>> maletajad |
Töötas! Proovime veel. Kui meil on sõnede järjend, sorteeritakse neid tavaliselt tähestiku järjekorras:
>>> sõned = ["aaaa", "b", "ccc", "ddddd", "ee"] |
Aga mis siis, kui tahame neid sorteerida sõnepikkuse järjekorras? Peaksime tegema funktsiooni, mis tagastab sõne pikkuse.
def sõne_pikkus(sõne): |
Oota, selline funktsioon on juba olemas ja seda kasutasime isegi siin funktsioonis: len. Paneme sorteerimisel parameetri key väärtuseks lihtsalt selle!
>>> sorted(sõned, key=len) |
Saime isegi meeldiva ja loetava koodi.
Kui funktsiooni pole veel kirjutatud, peame selle ise defineerima, nagu tegime esimeses näites. Üsna tüütu on nii lihtsat funktsiooni ise defineerida. Võiks ju olla lihtsam viis funktsiooni defineerimiseks ilma, et peab mitu rida koodi juurde kirjutama.
Et vähendada koodi kirjutamist, saame kasutada lambda-funktsioone. Need on funktsioonid, mida saab kirjutada lühidalt ja kasutada anonüümselt ehk neid ei pea muutujasse salvestama.
Vaatame funktsiooni, mida me enne kirjutasime:
def teine_liige(ennik): |
Seda saab samaväärselt kirjutada lambdana:
teine_liige = lambda ennik: ennik[1] |
Defineerisime just funktsiooni teine_liige, mis võtab parameetriks muutuja ennik ning tagastab selle teise liikme.
Saame neid samamoodi kasutada teiste funktsioonide parameetrites:
>>> maletajad |
Tegelikult on sellised funktsioonid ka juba Pythoni standardteegis olemas, aga nende kasutamiseks peab ühe mooduli importima ning nendega sai teha hea sissejuhatuse lambdadesse.
Vaatame veel funktsioone, mis nõuavad parameetritena teisi funktsioone.
Üks asi, mis võib tihti ette tulla, on kogu järjendi elementidele mingi funktsiooni rakendamine. Näiteks on vaja arvusõnede järjend muuta arvude järjendiks. Selle jaoks peaks need for-tsükliga läbi käima ja kuskile uude järjendisse lisama.
sõned = ["3", "1", "4", "1", "5", "9"] |
See on päris palju koodi nii lihtsa asja jaoks. Et säästa koodi kirjutamist, on Pythonis funktsioon map. Sellele saab parameetriteks panna ühe funktsiooni ning järjendi, et jooksutada seda funktsiooni kõikide järjendi elementide peal. See tagastab objekti, mida saab läbida for-tsükliga või muuta järjendiks funktsiooniga list. Eelmine kood uuesti, kasutades funktsiooni map:
sõned = ["3", "1", "4", "1", "5", "9"] |
Tegime palju lühema koodiga sama asja.
Võimalik, et funktsioon nõuab mitut parameetrit, näiteks round, mille esimene parameeter on ümardatav arv ja teine on kohtade arv, mitmeni peaks ümardama.
>>> round(3.14159265, 2) |
Sel juhul saab map parameetriteks panna 2 järjendit: esimesed parameetrid ja teised parameetrid. Ümardame näiteks ujukomaarvude järjendi arvud vastavalt teisele järjendile.
arvud = [3.14159, 2.71828, 1.41421, 6.28318, 1.61803] |
Proovi map funktsiooniga kõik järjendi sõned suurtähestada (sõne meetod upper).
Vahepeal on vaja järjendeid filtreerida. Näiteks tahame järjendist jätta alles ainult positiivsed arvud. Võib jälle teha for-tsükli:
arvud = [3, -6, 1, -2, 4, -8, 1, -3] |
Seda saab lühemalt teha funktsiooniga filter. See funktsioon nõuab parameetritesse funktsiooni ning järjendit. Tagastatud järjendisse jäetakse alles ainult need elemendid, mille peal funktsioon tagastab True. Eelmine koodijupp funktsiooniga filter:
arvud = [3, -6, 1, -2, 4, -8, 1, -3] |
Seda saab ka kasutada sõnede peal. Näiteks saame alles jätta kõik tähed ja tühikud tekstis ilma muude sümboliteta:
lause = "„Funktsionaalprogrammeerimine on lihtne,” ütles mitte keegi." |
Kuna filter tagastab järjendilaadse objekti, peab sõne saamiseks selle kokku liitma sõne meetodiga join.
Kasuta filter funktsiooni, et saada kätte salajane sõnum järgnevast tekstist:
XXXSXXXXXXAXXXXXXLXXXXXXAXXXXXXJXXXXXXAXXXXXXNXXXXXXEXXXXXX XXXXXXSXXXXXXÕXXXXXXNXXXXXXUXXXXXXMXXX |
Veel üks huvitav funktsioon on reduce.
Kui meil on mitu järjendit sarnaste andmetega ja me tahame neid kokku pakkida, siis peaksime need for-tsükliga indekseerima:
eesnimed = ["Jüri", "Mailis", "Tõnis", "Tanel", "Urmas"] |
Funktsioon zip lubab seda palju lihtsamalt teha:
eesnimed = ["Jüri", "Mailis", "Tõnis", "Tanel", "Urmas"] |
See on kasulik, kui meil on näiteks erinevaid andmeid samade asjade kohta ja me tahame neid kokku liita.
Funktsioonile zip võib sisse sööta lõpmatu arvu parameetreid:
>>> list(zip([1], [2], [3], [4], [5])) |
Seega, kui me sisestame saadud paarid zip funktsiooni parameetritesse, siis saame tagasi eesnimede ja perenimede järjendid:
>>> list(zip(('Jüri', 'Ratas'), ('Mailis', 'Reps'), ('Tõnis', 'Lukas'), ('Tanel', 'Kiik'), ('Urmas', 'Reinsalu'))) |
Aga kui meil on paarid järjendi sees, siis kuidas kõiki elemente funktsiooni parameetritesse saada?
Et järjendit funktsiooni parameetritena kasutada, tuleb järjend panna parameetriks ning selle ette kirjutada tärn. Proovime algul lihtsama näitega: paneme funktsiooni round parameetriks kaheliikmelise järjendi.
>>> round(*[3.14159265368, 2]) |
Proovime sama võtet zip funktsiooni peal:
>>> ministrid = [('Jüri', 'Ratas'), ('Mailis', 'Reps'), ('Tõnis', 'Lukas'), ('Tanel', 'Kiik'), ('Urmas', 'Reinsalu')] |
Töötab! Oleme edukalt järjendid kokku ja lahti pakkinud.
Kasutasime erinevaid võtteid, et vähendada üleliigset koodi kirjutamist. Võibolla tundub tõesti, et see teeb lihtsaid asju keerulisemaks, aga piisava harjutamisega muutuvad need lihtsamaks ning pikema koodi kirjutamise soov kaob ära.
Kuigi õpitud võtted lubavad lahendada peaaegu kõiki ülesandeid ühe reaga, peab meelde tuletama, et Pythoni ametlik stiiliõpetus nõuab, et read on maksimaalselt 79 tähemärgi pikkused. Päris programmides tuleks kood mõistlikult paigutada mitmele reale.
Paljud siin peatükis rakendatud funktsioonid on seotud funktsionaalprogrammeerimisega. Sellesse teemasse süveneb aine "Programmeerimiskeeled" (MTAT.03.006) keeltega Haskell ja Scala.
1. Antud on järjendid nimedest, matriklinumbritest ja keskmistest hinnetest.
nimed = ["Jaan", "Martin", "Katrin", "Margus", "Tiiu", "Jüri", "Anna", "Sirje", "Ülle", "Kristjan", "Anne", "Julia", "Andres", "Marina", "Rein", "Aivar", "Tiina", "Urmas", "Toomas", "Maria"] |
Kirjuta ühe reaga funktsioon, mis tagastab ennikud nendest, kelle keskmine hinne on vähemalt 4.6, sorteeritud keskmise hinde järgi kahanevalt.
>>> stipisaajad(nimed, matriklinumbrid, hinded) |
2. Tekstifaili evolutsioon.txt on salvestatud tekst imelikul kujul: tekst algab ülevalt paremalt ja liigub alla. Kirjuta ühe reaga funktsioon korrasta, mis võtab parameetrina failinime ning tagastab õiges järjekorras loetud teksti. Lahendamiseks piisab siin peatükis kasutatud funktsioonidest, v.a faili sisselugemine. Faili sulgemine siin ülesandes pole tähtis.
nne mE g,raon , fnsd audtl enl e vd mbs o hoes laasa vrvtuf eee to d wir .bbofm eenus iedl |
>>> with open("ül1.py") as f: print(len(f.readlines())) |